// SPDX-FileCopyrightText: Adam Evyčędo
//
// SPDX-License-Identifier: GPL-3.0-or-later

package xyz.apiote.bimba.czwek.repo

import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.drawable.Drawable
import android.util.Base64
import android.util.Log
import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.edit
import androidx.core.graphics.drawable.toDrawable
import androidx.preference.PreferenceManager
import com.auth0.android.jwt.JWT
import com.github.jershell.kbson.KBson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.openid.appauth.AuthState
import net.openid.appauth.AuthState.AuthStateAction
import net.openid.appauth.AuthorizationException
import net.openid.appauth.AuthorizationService
import net.openid.appauth.ClientSecretPost
import net.openid.appauth.EndSessionRequest
import org.minidns.hla.ResolverApi
import org.minidns.util.MultipleIoException
import xyz.apiote.bimba.czwek.R
import xyz.apiote.bimba.czwek.account.Client
import xyz.apiote.bimba.czwek.account.fragments.AccountFragment
import xyz.apiote.bimba.czwek.api.getImageFromUrl
import xyz.apiote.bimba.czwek.network.isNetworkAvailable
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.io.IOException
import java.security.MessageDigest


enum class Seat {
	PLATFORM,
	BACK,
	WINDOW,
	FRONT,
	DRIVER,
	PILOT,
	GARAGE;

	override fun toString(): String {
		return when (this) {
			PLATFORM -> "platform"
			BACK -> "back"
			WINDOW -> "window"
			FRONT -> "front"
			DRIVER -> "driver"
			PILOT -> "pilot"
			GARAGE -> "garage"
		}
	}

	companion object {
		fun fromString(name: String?): Seat {
			return when (name) {
				"platform" -> PLATFORM
				"back" -> BACK
				"window" -> WINDOW
				"front" -> FRONT
				"driver" -> DRIVER
				"pilot" -> PILOT
				"garage" -> GARAGE
				else -> throw Exception("unknown seat $name")
			}
		}
	}
}

data class User(val authState: AuthState?, var idToken: String?) {
	val displayName: String?
	val seat: Seat
	val emailAddress: String?
	val emailVerified: Boolean
	val matrixID: String?
	val matrixVerified: Boolean
	private var hasAvatar: Boolean = false

	init {
		if (idToken == null) {
			displayName = null
			seat = Seat.PLATFORM
			emailAddress = null
			emailVerified = false
			matrixID = null
			matrixVerified = false
		} else {
			val jwt = JWT(idToken!!)
			seat = Seat.fromString(jwt.getClaim("seat").asString())
			displayName = jwt.getClaim("display_name").asString()
			emailAddress = jwt.getClaim("email").asString()
			emailVerified = jwt.getClaim("email_verified").asBoolean() == true
			matrixID = jwt.getClaim("matrix_id").asString()
			matrixVerified = jwt.getClaim("matrix_verified").asInt() == 1
		}
	}

	fun getDisplayName(context: Context): String {
		// TODO vocative?
		return displayName ?: context.getString(R.string.anonymous_display_name)
	}

	fun save(context: Context) {
		if (authState == null && idToken == null) {
			PreferenceManager.getDefaultSharedPreferences(context).edit {
				remove("user")
			}
			return
		}
		val data = mapOf(
			"auth_state" to authState?.jsonSerializeString(),
			"id_token" to idToken,
			"has_avatar" to if (hasAvatar) {
				"true"
			} else {
				"false"
			}
		)
		val savedUser = Base64.encodeToString(
			KBson().dump(
				kotlinx.serialization.serializer<Map<String, String?>>(),
				data
			), Base64.DEFAULT
		)
		PreferenceManager.getDefaultSharedPreferences(context).edit {
			putString("user", savedUser)
		}
	}

	fun isAuthenticated(): Boolean {
		return authState?.isAuthorized == true
	}

	fun logout(context: Context): Intent {
		val endSessionRequest =
			EndSessionRequest.Builder(authState!!.authorizationServiceConfiguration!!)
				.setIdTokenHint(idToken)
				.setPostLogoutRedirectUri(AccountFragment.LOGOUT_CALLBACK)
				.build()
		User(null, null).save(context)
		val avatarFile = File(context.cacheDir, "avatar")
		try {
			avatarFile.delete()
		} catch (e: IOException) {
			Log.w("User", "Cannot delete avatar because: $e")
		}
		return AuthorizationService(context).getEndSessionRequestIntent(endSessionRequest)
	}

	private fun refreshProfile(context: Context) {
		if (!isNetworkAvailable(context)) {
			return
		}

		val client = Client.load(context)
		if (client == null) {
			return
		}

		authState?.performActionWithFreshTokens(
			AuthorizationService(context),
			ClientSecretPost(client.secret),
			object : AuthStateAction {
				override fun execute(
					accessToken: String?,
					idToken: String?,
					ex: AuthorizationException?
				) {
					save(context)
					if (ex != null) {
						Log.e("Refresh Profile", "refresh token failed: $ex")
						if (ex.error == "invalid_client") {
							Client.reset(context)
						}
						return
					}
					this@User.idToken = idToken
				}
			})

		GlobalScope.launch {
			getAvatar(context, true)
		}
	}

	suspend fun getAvatar(context: Context, force: Boolean = false): Drawable {
		return getLibravatar(context, force) ?: when (seat) {
			Seat.PLATFORM -> AppCompatResources.getDrawable(context, R.drawable.avatar_anonymous)!!
			Seat.BACK -> AppCompatResources.getDrawable(context, R.drawable.avatar_back)!!
			Seat.WINDOW -> AppCompatResources.getDrawable(context, R.drawable.avatar_window)!!
			Seat.FRONT -> AppCompatResources.getDrawable(context, R.drawable.avatar_front)!!
			Seat.DRIVER -> AppCompatResources.getDrawable(context, R.drawable.avatar_driver)!!
			Seat.PILOT -> AppCompatResources.getDrawable(context, R.drawable.avatar_pilot)!!
			Seat.GARAGE -> AppCompatResources.getDrawable(context, R.drawable.avatar_garage)!!
		}
	}

	@OptIn(ExperimentalStdlibApi::class)
	private suspend fun getLibravatar(context: Context, force: Boolean): Drawable? {
		val avatarFile = File(context.cacheDir, "avatar")
		if (!force && hasAvatar) {
			try {
				val stream = FileInputStream(avatarFile)
				return BitmapFactory.decodeStream(stream).toDrawable(context.resources)
			} catch (_: FileNotFoundException) {
				return null
			}
		}

		return emailAddress?.let {
			val md = MessageDigest.getInstance("SHA-256")
			md.update(emailAddress.encodeToByteArray())
			val hex = md.digest().toHexString()

			val url = try {
				val addresses = withContext(Dispatchers.IO) {
					ResolverApi.INSTANCE.resolveSrv("_avatars._tcp.${it.split("@")[1]}").sortedSrvResolvedAddresses
				}

				if (addresses.isEmpty()) {
					"https://seccdn.libravatar.org/avatar/$hex?d=404&s=64"
				} else {
					val host = addresses[0].srv.target.replace(Regex("\\.$"), "")
					val port = addresses[0].srv.port
					if (port != 443) {
						"http://$host:$port/avatar/$hex?d=404&s=64"
					} else {
						"https://$host/avatar/$hex?d=404&s=64"
					}
				}
			} catch (e: IllegalStateException) {
				Log.w("User", "Cannot resolve libravatar SRV entry because: $e")
				"https://seccdn.libravatar.org/avatar/$hex?d=404&s=64"
			} catch (_: MultipleIoException) {
				"https://seccdn.libravatar.org/avatar/$hex?d=404&s=64"
			}

			val drawable = getImageFromUrl(context, url)
			drawable?.let {
				val stream = FileOutputStream(avatarFile)
				drawable.bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
				stream.close()
			}
			hasAvatar = true
			save(context)
			return drawable
		}
	}

	companion object {
		fun load(context: Context): User {
			val savedUser = PreferenceManager.getDefaultSharedPreferences(context).getString("user", null)
			if (savedUser == null) {
				return User(null, null)
			}

			val userMap = KBson().load(
				kotlinx.serialization.serializer<Map<String, String?>>(),
				Base64.decode(savedUser, Base64.DEFAULT)
			)
			val user =
				User(AuthState.jsonDeserialize(userMap["auth_state"]!!), userMap["id_token"]).apply {
					hasAvatar = userMap["has_avatar"] == "true"
				}

			user.refreshProfile(context)

			return if (!user.isAuthenticated()) {
				Log.i("User", "User unauthenticated, returning null")
				User(null, null)
			} else {
				user
			}
		}
	}
}